package com.btcode.web.mvc.servlet;

import com.btcode.common.ExecuteTimeUtil;
import com.btcode.common.MyLog;
import com.btcode.exception.MsgException;
import com.btcode.log.ILog;
import com.btcode.reflect.IClassLoader;
import com.btcode.web.core.annotation.RequestMapping;
import com.btcode.web.core.controller.Action;
import com.btcode.web.core.controller.Controller;
import com.btcode.web.core.errorhandle.IErrorHandler;
import com.btcode.web.mvc.errorhandle.DefaultApiErrorHandler;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Response;

/**
 * url映射访问器
 *
 * @author gislin
 */
public class ApiUrlHandleServlet implements Servlet {

    private List<ApiPackScanInfo> baseScanPackets;

    private ILog log;

    /**
     * 项目的根地址，相当于项目名
     */
    private String contextPath;

    /**
     * 请求前缀，在地址解析的时候，会把地址中的这部分去掉
     */
    private String urlPrefix;

    private HashMap<String, Action> acctionMapping = new HashMap<>();

    private static List<String> urls = new ArrayList<>();

    private IClassLoader classLoader;

    private IErrorHandler errorHandler;

    private boolean isUrlMappingCheck;

    /**
     *
     * @param baseScanPacket 需要扫描的包集合，用于加载业务接口
     * @param urlPrefix 地址前缀，在所有接口前，加入前缀，方便做某些接口过滤
     * @param classLoader 类加载策略，用于找到对应的接口类，并加载
     * @param isUrlMappingCheck 是否开启强制地址匹配检查，若开启，@RequestMapping(path = "")中的path，必须和包路径一一匹配
     */
    public ApiUrlHandleServlet(List<ApiPackScanInfo> baseScanPacket, String urlPrefix,
        IClassLoader classLoader, boolean isUrlMappingCheck) {
        this.baseScanPackets = baseScanPacket;
        this.urlPrefix = urlPrefix;
        this.classLoader = classLoader;
        this.isUrlMappingCheck = isUrlMappingCheck;
        log = MyLog.getInstance().getLogger(ApiUrlHandleServlet.class.getName());
    }

    @Override
    public void destroy() {
        // TODO Auto-generated method stub

    }

    @Override
    public ServletConfig getServletConfig() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public String getServletInfo() {
        // TODO Auto-generated method stub
        return null;
    }

    public void setErrorHandler(IErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    public static List<String> getUrls() {

        /**
         * 做深拷贝，防止被外部改动
         */
        List<String> myUrls = new ArrayList<>();

        for (String url : urls) {
            myUrls.add(url);
        }

        return myUrls;
    }

    @Override
    public void init(ServletConfig arg0) throws ServletException {

        try {
            List<Controller> controllers = getController(baseScanPackets);

            contextPath = arg0.getServletContext().getContextPath();

            for (Controller controller : controllers) {
                Method[] methods = controller.getClass().getMethods();

                RequestMapping rootRequestMapping = controller.getClass().getAnnotation(
                    RequestMapping.class);
                String rootPath = rootRequestMapping.path();

                if (!rootPath.startsWith("/")) {
                    rootPath = "/" + rootPath;
                }

                /**
                 * 是否检查url和包名一一对应
                 */
                if (isUrlMappingCheck) {
                    urlMappingCheck(controller, rootPath);
                }

                for (Method method : methods) {
                    RequestMapping actionRequestMapping = method.getAnnotation(
                        RequestMapping.class);
                    if (actionRequestMapping == null) {
                        continue;
                    }

                    String actionPath = actionRequestMapping.path();

                    if (!actionPath.startsWith("/")) {
                        actionPath = "/" + actionPath;
                    }

                    String urlPath = rootPath + actionPath;

                    if (isUrlMappingCheck && urlPath.matches(".*[A-Z]+.*")) {
                        throw new MsgException(
                            "控制器" + controller.getClass().toString() + "映射路径有误,不应有大写字母");
                    }

                    if (acctionMapping.containsKey(urlPath)) {

                        Action action = acctionMapping.get(urlPath);

                        log.info("地址覆盖：" + controller.getClass() + " 覆盖 " + urlPath + action
                            .getController().getClass());
                    }

                    acctionMapping.put(urlPath, new Action(urlPath, method, controller));
                    urls.add(urlPrefix + urlPath);
                    if (log != null) {
                        log.info("映射:[" + urlPath + "] > " + controller.getClass().getName() + "."
                                     + method.getName());
                    }
                }
            }

        }
        catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        catch (InstantiationException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 检查url合法性，合法性检查规则：
     * 比如有一个项目结构如下：
     * |—com.bt.common
     * |——usersafe
     * |———function
     * |————FunctionController
     * |——safe
     * 如果扫描的根目录为，com.bt.common.usersafe,那么针对FunctionController,其RequestMapping的path应该为/usersafe/function/
     * 如果扫描的根目录为，com.bt.common，那么针对FunctionController，其RequestMapping的path应该为/common/usersafe/function/
     * 原则就是，会把上一级的目录跟进来
     */
    private void urlMappingCheck(Controller controller, String rootUrlPath) {
        String currentPackName = controller.getClass().getPackage().getName();
        String packPath = "";

        for (ApiPackScanInfo baseScanPacket : baseScanPackets) {

            String packetPrix = baseScanPacket.getPackScanPrix();

            if (currentPackName.startsWith(packetPrix)) {
                //把最后以及目录去掉
                packetPrix = packetPrix.substring(0,packetPrix.lastIndexOf("."));
                packPath = currentPackName.replaceFirst(packetPrix, "");
                break;
            }
        }

        packPath = packPath.replaceAll("\\.", "/").toLowerCase();

        if (!packPath.equals(rootUrlPath.toLowerCase())) {

            throw new MsgException(
                "控制器" + controller.getClass().toString() + "映射路径有误,路径应为：【" + packPath + "】");
        }
    }

    @Override
    public void service(ServletRequest arg0, ServletResponse arg1)
        throws ServletException, IOException {

        HttpServletRequest request = (HttpServletRequest) arg0;

        /**
         * 地址前缀是自定义的，去掉
         */
        String path = request.getRequestURI().replaceFirst(urlPrefix, "");
        /**
         * 把项目地址去掉，项目地址是自定义的，并不真正包含在物理路径里
         */
        path = path.replaceFirst(contextPath, "");
        /**
         * 获取对应的action
         */
        Action action = acctionMapping.get(path);
        /**
         * 如果有异常的话
         */
        Throwable exception = null;

        if (action == null) {

            Response response = (Response) arg1;
            response.setStatus(HttpStatus.NOT_FOUND_404);

            if (log != null) {
                log.error("无法处理api地址:" + path);
            }
            return;
        }

        try {
            ExecuteTimeUtil executeTime = new ExecuteTimeUtil();
            action.invoke(arg0, arg1);
            double timeCost = (double)(executeTime.getWholeDiff());
            double costInSecond = timeCost / 1000d;
            log.info(path + ",业务逻辑处理耗时：" + costInSecond + "s");
        }
        catch (IllegalArgumentException e) {
            String msgStr =
                path + "请求出现错误,请确认对应的controller方法的参数是否为：ServletRequest arg0, ServletResponse arg1";
            log.error(msgStr, e);
            exception = new Throwable(msgStr, e);
        }
        catch (IllegalAccessException e) {
            log.error(path + "请求出现错误(01)", e);

            exception = new Throwable(path + "请求出现错误(01)", e);
        }
        catch (InvocationTargetException e2) {

            log.error(path + "请求出现错误(02)", e2);

            Throwable targetException = e2.getTargetException();

            if (targetException == null) {
                exception = new Throwable(path + "请求出现错误(02)", e2);
            }
            else {
                exception = new Throwable(targetException.getMessage(), e2);
            }
        }

        if (exception == null) {
            return;
        }

        if (errorHandler == null) {
            errorHandler = new DefaultApiErrorHandler();
        }

        errorHandler.handleError(arg0, arg1, exception);
    }

    /**
     *
     * @param
     * @return
     * @throws ClassNotFoundException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws IOException
     */
    private List<Controller> getController(List<ApiPackScanInfo> packScanInfos)
        throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
        List<Class<?>> myClasses = new ArrayList<>();

        for (ApiPackScanInfo packetInfo : packScanInfos) {
            myClasses.addAll(
                classLoader.loadClass(packetInfo.getTargetClass(), packetInfo.getPackScanPrix()));
        }

        List<Controller> controllers = new ArrayList<>();

        for (Class<?> mClass : myClasses) {
            controllers.add((Controller) mClass.newInstance());
        }

        return controllers;
    }
}
